Skip to content

ThreadLocal简介 ​

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量,JDK提供的ThreadLocal类正是为了解决这样的问题。Threadlocal 类主要解决的就是让每个线程绑定自己的值。可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本。这也是ThreadLocal变量名的由来。他们可以使用get()和set()方法来获取默认值或将其值更改为当前线程所存的副本的值。这样就避免了线程安全问题。

再举个简单的例子:

比如有两个人去宝屋收集宝物。这两个人共用一个袋子肯定会产生争执。但是给他们两个人每个人分配一个袋子,就不会出现这样的问题。如果把这两个人比作线程,那么ThreadLocal就是用来避免这两个线程竞争的。

ThreadLocal的创建并不是为了解决资源共享和并发问题。

ThreadLocal代码示例 ​

相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。

java
import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable {

	private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>() {
		@Override
		protected SimpleDateFormat initialValue() {
			return new SimpleDateFormat("yyyyMMdd HHmm");
		}
	};

	public static void main(String[] args) throws InterruptedException {
		ThreadLocalExample threadLocalExample = new ThreadLocalExample();
		for (int i = 0; i < 10; i++) {
			Thread thread = new Thread(threadLocalExample, "" + i);
			Thread.sleep(new Random().nextInt(1000));
			thread.start();
		}
	}

	@Override
	public void run() {
		System.out.println("Thread Name= " + Thread.currentThread().getName() + " default Formatter = " + formatter.get().toPattern());

		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		// formatter pattern is changed here by thread, but it won't reflect to other
		// threads
		formatter.set(new SimpleDateFormat());

		System.out.println("Thread Name = " + Thread.currentThread().getName() + " formatter = " + formatter.get().toPattern());
	}

}

Output:

java
Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name = 0 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name = 1 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name = 2 formatter = yy-M-d ah:mm
Thread Name = 3 formatter = yy-M-d ah:mm
Thread Name = 4 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name = 5 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name = 6 formatter = yy-M-d ah:mm
Thread Name = 7 formatter = yy-M-d ah:mm
Thread Name = 8 formatter = yy-M-d ah:mm
Thread Name = 9 formatter = yy-M-d ah:mm

从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同。其他线程也一样。

上面创建 ThreadLocal 变量的那段代码用到了 Java8 的知识,它等于下面这段代码。如果你写了下面这段代码,IDEA会提示你转换为Java8的格式。因为ThreadLocal类在Java 8中扩展,使用一个新的方法withInitial(),将Supplier功能接口作为参数。

java
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
    @Override
    protected SimpleDateFormat initialValue()
    {
        return new SimpleDateFormat("yyyyMMdd HHmm");
    }
};

ThreadLocal原理 ​

从 Thread类源代码入手。

java
public class Thread implements Runnable {
     ......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
     ......
}

从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量。它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是null。只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们。调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set() 方法。

ThreadLocal类的set()方法

java
/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

通过上面这些内容,我们足以通过猜测得出结论:最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。 比如我们在同一个线程中声明了两个 ThreadLocal 对象,会使用 Thread内部的ThreadLocalMap变量存放数据。ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。ThreadLocal内部维护的是一个类似Map的ThreadLocalMap数据结构,key为当前对象的ThreadLocal对象,值为泛型的Object。这也就解释了ThreadLocal声明的变量为什么在每一个线程都有自己的专属本地变量。

ThreadLocalMap是ThreadLocal的静态内部类。

ThreadLocalMap和ThreadLocal的UML图

首先来看看为什么ThreadLocal会产生内存泄漏 ​

ThreadLocal

ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表。这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。

也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。 值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

ThreadLocalMap使用ThreadLocal的弱引用作为key,而value是强引用。所以,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收——key会被清理掉,而value不会被清理掉。这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。如果当前线程再迟迟不结束,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value。这个 value 永远无法回收,造成内存泄漏。

ThreadLocalMap 的实现中已经考虑了这种情况,在调用set()、get()、remove()方法的时候,会清理掉key为null的纪录。

java
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

弱引用介绍

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象。不管当前内存空间足够与否,都会回收它的内存。不过,垃圾回收器是一个优先级低的线程。因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用。弱引用所引用的对象被垃圾回收时,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

关于弱引用可以参照下面链接了解

<a href="https://github.com/DemoTransfer/demotransfer/blob/master/java/interview/Java 并发/java基础/强引用 、软引用、 弱引用、虚引用.md">强引用 、软引用、 弱引用、虚引用</a>

如何避免ThreadLocal内存泄漏问题 ​

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。在使用线程池的情况下,立即清理ThreadLocal,规避内存泄漏问题的发生。